﻿# -*- coding: utf-8 -*-
""" Datum / Projection Tree Model/View

    :Author:
      - 20111221-20120110 Roberto Vidmar

    :Revision:  $Revision: 52 $
                $Date: 2012-11-12 09:07:15 +0000 (Mon, 12 Nov 2012) $

    :Copyright: 2011-2012
                Nicola Creati <ncreati@inogs.it>
                Roberto Vidmar <rvidmar@inogs.it>

    :License: MIT/X11 License (see :download:`license.txt
                               <../../license.txt>`)
"""

import os
import sys
from pyproj import Proj, pyproj_datadir
from UserDict import UserDict

from qtCompat import QtCore, Qt, QtGui
QCA = QtCore.QCoreApplication

try:
  from pyproj import pj_list
except ImportError:
  pj_list = None

CUSTOMPROJECTION = QCA.translate("projMVC", "User defined Coordinate System")
CUSTOMCOLOR = QCA.translate("projMVC", "lightsteelblue")
EPSGBRANCHCOLOR = QCA.translate("projMVC", "lightgray")
CUSTOMSEPCHR = "|"
DATUMSHIFT_KEYWORD = "+datumShift="
GEOIDGRIDS_KEYWORD = "+geoidgrids="
EXTRA_KEYWORDS = (DATUMSHIFT_KEYWORD, GEOIDGRIDS_KEYWORD)

def pyprojEPSGDict(epsgfile=None):
  """ Return an ordered dictionary from epsg data file defined by pyproj

    :param epsgfile: EPSG file path
    :type epsgfile: string, unicode
    :returns: ordered dictionary from epsg data file defined by pyproj
    :rtype: Odict
    :raises:
  """
  if epsgfile is None:
    epsgfile = os.path.join(
      os.path.dirname(sys.argv[0]), pyproj_datadir, 'epsg')

  epsg = open(epsgfile, 'r').readlines()
  epsgDict = Odict()

  if pj_list is None:
    # Workaround for old versions of pyproj, needs proj-bin package
    projDict = dict()
    for proj in os.popen("/usr/bin/cs2cs -l").read().split("\n"):
      if proj:
        keyword = proj.split(':')[0].strip()
        descr = proj.split(':')[1].strip()
        projDict[keyword] = descr
  else:
    projDict = pj_list

  for line in epsg:
    if line[0] == '#':
      title = line[2: -1]
    if line[0] == '<':
      elements = line.split(' ')
      epsgNumber = elements[0][1: -1]

      projection = elements[1].partition('+proj=')[2]
      projection = projDict[projection]
      projString = " ".join(elements[1:-2])

      if epsgDict.has_key(projection):
        # Append
        epsgDict[projection].append((epsgNumber, title, projString))
      else:
        # Create
        epsgDict[projection] = [(epsgNumber, title, projString)]

  return epsgDict

#===============================================================================
class Odict(UserDict):
  """ An ordered dictionary implementation. """

  def __init__(self, d=None):
    """ Create a new ordered dictionary instance from an optional dictionary

      :param d: an optional dictionary from where to get the new dict keys
      :type d: dict
    """

    self._keys = []
    UserDict.__init__(self, d)

  def __delitem__(self, key):
    UserDict.__delitem__(self, key)
    self._keys.remove(key)

  def __setitem__(self, key, item):
    UserDict.__setitem__(self, key, item)
    if key not in self._keys: self._keys.append(key)

  def clear(self):
    UserDict.clear(self)
    self._keys = []

  def copy(self):
    """ Return a copy of the object

      :returns: a copy of the instance
      :rtype: Odict
    """
    d = UserDict.copy(self)
    d._keys = self._keys[:]
    return d


  def items(self):
    """ Return the items in the object

      :returns: the items in the object
      :rtype: various
    """
    return zip(self._keys, self.values())


  def keys(self):
    """ Return the keys in the object

      :returns: the keys in the object
      :rtype: various
    """
    return self._keys

  def popitem(self):
    """ Return the last element in the object and remove it

      :returns: the last element in the object
      :rtype: object's type
      :raises: KeyError
    """
    try:
      key = self._keys[-1]
    except IndexError:
      raise KeyError('dictionary is empty')

    val = self[key]
    del self[key]
    return (key, val)

  def setdefault(self, key, failobj=None):
    UserDict.setdefault(self, key, failobj)
    if key not in self._keys: self._keys.append(key)

  def update(self, dict):
    UserDict.update(self, dict)
    for key in dict.keys():
      if key not in self._keys: self._keys.append(key)

  def values(self):
    """ Return the values in the object

      :returns: the values in the object
      :rtype: various
    """
    return map(self.get, self._keys)

#===============================================================================
class BranchNode(object):
  """ Branch Node implementation
  """
  def __init__(self, name, parent=None):
    """ Create a new BranchNode instance.

      :param name: Name of the branch
      :type name: string
      :param parent: parent node
      :type parent: BranchNode instance
      :raises:
    """
    super(BranchNode, self).__init__()
    self._name = name
    self._parent = parent
    self._children = []

  def __len__(self):
    return len(self._children)

  def __str__(self):
    return self._name

  def parent(self):
    """ Return parent node

      :returns: parent node
      :rtype: BranchNode
      :raises:
    """
    return self._parent

  def children(self):
    return self._children

  def name(self):
    """ Return node name

      :returns: node name
      :rtype: string
      :raises:
    """
    return self._name

  def __repr__(self):
    return self.__str__()

  def childAtRow(self, row):
    return self._children[row]

  def appendChild(self, child):
    child._parent = self
    self._children.append(child)

  def removeChildAtRow(self, row):
    """ Remove child at rowa `row`

      :param row: number of the row to remove (starting from 0)
      :type name: int
      :raises:
    """
    assert 0 <= row < len(self._children)
    self._children.pop(row)

  def rowOfChild(self, child):
    for i, item in enumerate(self._children):
      if item == child:
        return i
    return -1

  def isBranch(self):
    """ Return True

      :returns: True
      :rtype: bool
      :raises:
    """
    return True

  def isLeaf(self):
    """ Return False

      :returns: False
      :rtype: bool
      :raises:
    """
    return False

#===============================================================================
class LeafNode(object):
  """ Leaf Node implementation
  """
  def __init__(self, entry, parent=None):
    """ Create a new LeafNode instance.

      :param entry: (epsg, name, value)
      :type entry: tuple
      :param parent: parent node
      :type parent: BranchNode instance
      :raises:
    """
    super(LeafNode, self).__init__()

    self._parent = parent
    self._name = entry[1]
    self._epsg = entry[0].strip()
    self._projStr = entry[2].strip()

  def __len__(self):
    return 3

  def children(self):
    """ Return node children: the empty list

      :returns: []
      :rtype: list
      :raises:
    """
    return []

  def __str__(self):
    return "%s: %7s '%s'" % (self._name, self._epsg, self._projStr)

  def __repr__(self):
    return self.__str__()

  def parent(self):
    """ Return parent node

      :returns: parent node
      :rtype: BranchNode
      :raises:
    """
    return self._parent

  def childAtRow(self, row):
    return self._parent.childAtRow(row)

  def field(self, column):
    if column == 0:
      return self._name
    elif column == 1:
      return self._epsg
    elif column == 2:
      return self._projStr

  def name(self):
    """ Return node name

      :returns: node name
      :rtype: string
      :raises:
    """
    return self._name

  def setName(self, name):
    """ Set node name to `name`

      :param name: node name
      :type name: string
      :raises:
    """
    self._name = name

  def epsg(self):
    """ Return EPSG code

      :returns: EPSG code
      :rtype: string
      :raises:
    """
    return self._epsg

  def projStr(self):
    """ Return proj string

      :returns: PROJ.4 string
      :rtype: string
      :raises:
    """
    return self._projStr

  def getMyRow(self):
    """ Return row of this node into model

      :returns: row of this node into model
      :rtype: int
      :raises:
    """
    return self._parent.rowOfChild(self)

  def isBranch(self):
    """ Return False

      :returns: False
      :rtype: bool
      :raises:
    """
    return False

  def isLeaf(self):
    """ Return True

      :returns: True
      :rtype: bool
      :raises:
    """
    return True

#===============================================================================
class ProjModel(QtCore.QAbstractItemModel):
  """ Datum / Projection TreeView Model
  """
  CUSTOMFILENAME = os.path.join("~", ".custom_projections")
  def __init__(self, parent=None):
    """ Create a new ProjModel instance.

      :param parent: parent object
      :type parent: QObject
      :raises:
    """
    super(ProjModel, self).__init__(parent)
    self.headers = [QCA.translate("ProjModel", "Coordinate Reference System"),
      QCA.translate("ProjModel", "EPSG"), QCA.translate("ProjModel", "Proj.4")]
    self.columns = len(self.headers)
    self.root = BranchNode('')

  def columnCount(self, parent):
    """ Return number of columns in the model

      :returns: number of columns in the model
      :rtype: int
      :raises:
    """
    return self.columns

  def rightmostIndex(self, index):
    """ Return index of rightmost item in the row of index

      :param index: index of an intem in the tree
      :type index: QModelIndex
      :returns: QModelIndex of rightmost item in the row of index
      :rtype: QModelIndex
      :raises:
    """
    node = self.nodeFromIndex(index)
    return self.createIndex(index.row(), self.columns, node.parent())

  def makeNewName(self, sepchr='-'):
    """ Return a new name for a user defined Datum / Projection

      :param sepchr: separation character
      :type sepchr: string
      :returns: new name for a user defined Datum / Projection
      :rtype: string
      :raises:
    """
    name = QCA.translate("ProjModel", "New Projection") + sepchr + '1'
    namelist = [child.name() for child in self.root.children()[0].children()]
    if name not in namelist:
      return name
    else:
      if sepchr in name:
        base, dummy, ext = name.rpartition(sepchr)
      else:
        base, ext = name, '0'
      i = 1
      name = "%s%s%d" % (base, sepchr, i)
      while name in namelist:
        i += 1
        name = "%s%s%d" % (base, sepchr, i)
      return name

  def addCustom(self, value, epsgString=''):
    """ Return QModelIndex of new user defined (custom) Datum / Projection

       + NEW DATA WILL BE ADDED TO THE MODEL

      :param value: custom projection
      :type value: string
      :param epsgString: optional EPSG string
      :type epsgString: string
      :returns: QModelIndex of new user defined (custom) Datum / Projection
      :rtype: QModelIndex
      :raises:
    """
    new = (epsgString, self.makeNewName(), value)
    userBranch = self.root.children()[0]
    # Create a new (custom) projection
    leaf = LeafNode(new, userBranch)

    self.layoutAboutToBeChanged.emit()
    row = len(userBranch)
    self.beginInsertRows(self.createIndex(row, 0, userBranch), row, row)
    userBranch.appendChild(leaf)
    leafIndex = self.createIndex(row, 0, leaf)
    self.endInsertRows()
    self.layoutChanged.emit()
    return leafIndex

  def loadCustom(self, branch):
    """ Load custom projections from file to branch node

      :param branch: branch node
      :type branch: BranchNode
      :raises:
    """
    custom = self.root.children()[0]
    try:
      fid = open(os.path.expanduser(self.CUSTOMFILENAME), 'r')
      projections = fid.readlines()
      if projections:
        for line in projections:
          if line[0] == '#':
            # Comment
            comment = line[2: -1]
          else:
            elements = line.split(CUSTOMSEPCHR)
            name = elements[0]
            projString = elements[1].strip()
            epsgString = elements[2].strip()
            # Append a projection
            customTuple = (epsgString, name, projString)
            custom.appendChild(LeafNode(customTuple, branch))
    except IOError:
      pass

  def saveCustom(self):
    """ Save custom projections to well known :class:`CUSTOMFILENAME`
    """
    userBranch = self.root.children()[0]
    # Save the projection in our custom projections file
    fid = open(os.path.expanduser(self.CUSTOMFILENAME), 'w')
    for custom in userBranch.children():
      fid.write("%s%s  %s%s %s\n" %
        (custom.name(), CUSTOMSEPCHR, custom.projStr(), CUSTOMSEPCHR,
        custom.epsg()))
    fid.close()

  def setData(self, index, value, role=Qt.DisplayRole):
    """ Custom Reimplementation of the `setData` method.

      .. note:: DATA IN THE MODEL WILL CHANGE AFTER THIS CALL

      .. note:: After changing the node data, models must emit the dataChanged()
        signal to inform other components of the change.

      .. warning:: We go through setData twice if we press <Enter>
    """
    success = False
    if (index.internalPointer() == self.root.children()[0] or
      index.parent().internalPointer() == self.root.children()[0]) :
      userBranch = self.root.children()[0]
      if role == Qt.EditRole:
        #svalue = str(value.toString())
        svalue = value
        if not svalue:
          return False
        success = True
        # Avoid duplicates
        for child in index.parent().internalPointer().children():
          if child.name() == svalue:
            success = False
            break
        if success:
          #index.internalPointer().setName(str(value.toString()))
          index.internalPointer().setName(value)

    if success:
      self.dataChanged.emit(index, self.rightmostIndex(index))
      # Update file with custom projections
      self.saveCustom()

    return success

  def data(self, index, role):
    """ Custom Reimplementation of the `data` method.

        + Return the data stored under the given role for the item referred
          to by the index
    """
    node = index.internalPointer()
    if role == Qt.DecorationRole:
      if index.column() == 0:
        if isinstance(node, BranchNode):
          testIfProjected = "LAT/LONG" in node.name().upper()
        else:
          # Look for longlat in Proj.4 string
          testIfProjected = "longlat" in node.projStr()
        if testIfProjected:
          return QtGui.QPixmap(':/icon_geographic.png')
        elif node.name() == CUSTOMPROJECTION:
          return QtGui.QPixmap(':/icon_user.png')
        else:
          return QtGui.QPixmap(':/icon_projection.png')
    elif role == Qt.FontRole:
      if isinstance(node, BranchNode):
        myFont = QtGui.QFont()
        myFont.setBold(True)
        return myFont
    elif role == Qt.BackgroundColorRole:
      if isinstance(node, BranchNode):
        if node.name() == CUSTOMPROJECTION:
          return QtGui.QColor(CUSTOMCOLOR)
        else:
          return QtGui.QColor(EPSGBRANCHCOLOR)
    elif role == Qt.TextAlignmentRole:
      return int(Qt.AlignTop|Qt.AlignLeft)
    elif role == Qt.ToolTipRole:
      toolTip = ""
      userToolTip = ""
      if (index.internalPointer() == self.root.children()[0] or
        index.parent().internalPointer() == self.root.children()[0]) :
        userBranch = self.root.children()[0]
        # User defined area
        userToolTip = QCA.translate("ProjModel",
          ".\nRight click on this item to add\n"
          "your custom Coordinate System.\nDouble click to rename it\n"
          "Press <DEL> key to delete it.")

      if isinstance(node, BranchNode):
        if index.column() == 0:
          toolTip = QCA.translate("ProjModel",
            'The "%s" group%s') % (str(node), userToolTip)
      else:
        # Leaf Node
        if index.column() == 0:
          if userToolTip:
            toolTip = (QCA.translate("ProjModel",
              'User defined projection "%s"%s') %
              (node.field(index.column()), userToolTip))
          else:
            toolTip = QCA.translate("ProjModel",
              'EPSG projection "%s"') % node.field(index.column())
        elif index.column() == 1:
          if node.field(1):
            if userToolTip:
              toolTip = (QCA.translate("ProjModel",
                "Original EPSG code was %s%s") %
                (node.field(1), userToolTip))
            else:
              toolTip = QCA.translate("ProjModel",
                "EPSG code is %s%s") % (node.field(1), userToolTip)
          else:
            toolTip = userToolTip[2:]
      return toolTip
    elif role == Qt.DisplayRole:
      if isinstance(node, BranchNode):
        return str(node) if index.column() == 0 else ""
      else:
        return node.field(index.column())
    return

  def flags(self, index):
    """ Custom Reimplementation of the `flags` method.

        Return the item flags for the given index.
        The base class implementation returns a combination of flags that
        enables the item (ItemIsEnabled) and allows it to be selected
        (ItemIsSelectable). See Qt Documentation.
    """
    if (index.parent().internalPointer() == self.root.children()[0] and
      index.column() == 0):
      return (Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable)
    else:
      return Qt.ItemIsEnabled | Qt.ItemIsSelectable

  def headerData(self, section, orientation, role):
    """ Set header value model
    """
    if orientation == Qt.Horizontal and role == Qt.DisplayRole:
      return self.headers[section]
    return

  def index(self, row, column, parentIndex):
    """ Custom Reimplementation of the `index` method.

        Return the QModelIndex for the element at (row, column) given
        parentIndex

     .. note:: Must be implemented
    """
    assert self.root
    branch = self.nodeFromIndex(parentIndex)
    if row < len(branch):
      return self.createIndex(row, column, branch.childAtRow(row))
    else:
      return QtCore.QModelIndex()

  def load(self):
    """ Populate the QTreeModel loading data with `pyprojEPSGDict()`
    """
    epsgDict = pyprojEPSGDict()
    # Custom Projections Branch
    custom = BranchNode(CUSTOMPROJECTION)
    self.root.appendChild(custom)

    # First the EPSG file
    for key in epsgDict.keys():
      branch = BranchNode(key)
      self.root.appendChild(branch)

      for entry in epsgDict[key]:
        branch.appendChild(LeafNode(entry, branch))

    # Then user defined projection
    self.loadCustom(branch)
    self.reset()
    return

  def indexOfCol0(self, index):
    """ Return QModelIndex of item at column 0

      :param index: QModelIndex
      :type index: QModelIndex
      :returns: QModelIndex of item at column 0
      :rtype: QModelIndex
      :raises:
    """
    return self.createIndex(index.row(), 0, index.internalPointer())

  def nodeFromIndex(self, index):
    """ Return node (BranchNode or LeafNode) instance from QModelIndex

      :param index: QModelIndex
      :type index: QModelIndex
      :returns: (BranchNode or LeafNode) instance from QModelIndex
      :rtype: BranchNode or LeafNode
      :raises:
    """
    return index.internalPointer() if index.isValid() else self.root

  def parent(self, child):
    """ Return parent QModelIndex of `child`

      :param child: QModelIndex
      :type child: QModelIndex
      :returns: parent QModelIndex of `child`
      :rtype: QModelIndex
      :raises:
    """
    node = self.nodeFromIndex(child)
    if node is None:
      return QtCore.QModelIndex()
    parent = node.parent()
    if parent is None:
      return QtCore.QModelIndex()
    grandparent = parent.parent()
    if grandparent is None:
      return QtCore.QModelIndex()
    row = grandparent.rowOfChild(parent)
    return self.createIndex(row, 0, parent)

  def removeRecord(self, index):
    """ Remove only items in the first branch (user defined records).

      :param index: QModelIndex
      :type index: QModelIndex
      :returns: True on success
      :rtype: bool
      :raises:
    """
    userBranch = self.root.children()[0]
    node = self.nodeFromIndex(index)
    if node.parent() == userBranch:
      row = index.row()
      self.layoutAboutToBeChanged.emit()
      self.beginRemoveRows(self.createIndex(0, 0, userBranch), row, row)
      userBranch.removeChildAtRow(row)
      self.endRemoveRows()
      self.layoutChanged.emit()
      return True
    return False

  def rowCount(self, parent):
    """ Return number of rows in parent tree

      :param parent: QModelIndex
      :type parent: QModelIndex
      :returns: number of rows in parent node
      :rtype: int
      :raises:
    """
    node = self.nodeFromIndex(parent)
    if node is None or isinstance(node, LeafNode):
      return 0
    return len(node)

  def search(self, s, matchCase=False, exact=False):
    """ Recursively search the model for string s in (name, epsg, projString)

      :param s: string to search
      :type s: string
      :param matchCase: ignore case in search if True
      :type matchCase: bool
      :param exact: exact match if True, contained if False
      :type exact: bool
      :returns: list of items that match
      :rtype: list
      :raises:
    """
    s = s.strip()
    foundItems = []
    def match(s, ss):
      if matchCase:
        if exact:
          return s == ss
        else:
          return s in ss
      else:
        if exact:
          return s.upper() == ss.upper()
        else:
          return s.upper() in ss.upper()

    def loop(node):
      """ Iterate over the tree model
      """
      for row, child in enumerate(node.children()):
        if isinstance(child, BranchNode):
          loop(child)
        else:
          # Leaf
          if match(s, child.name()):
            foundItems.append(
              self.createIndex(row, 1, child.childAtRow(row)))
          elif match(s, child.epsg()):
            foundItems.append(
              self.createIndex(row, 0, child.childAtRow(row)))
          elif match(s, child.projStr()):
            foundItems.append(
              self.createIndex(row, 2, child.childAtRow(row)))

    loop(self.root)
    return foundItems

#===============================================================================
class ProjView(QtGui.QTreeView):
  """ Projection main TreeView based on MVC design
  """
  def __init__(self, parent=None):
    """ Create a new ProjView instance.

      :param parent: parent object
      :type parent: QObject
      :raises:
    """
    super(ProjView, self).__init__(parent)

    self.setUniformRowHeights(True)
    self.setAlternatingRowColors(True)
    self.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
    #self.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
    #self.setSelectionMode(QtGui.QAbstractItemView.NoSelection)

    # Context menu
    self.setContextMenuPolicy(Qt.CustomContextMenu)
    self.customContextMenuRequested.connect(self.contextMenuRequested)
    self.customProjStr = ''

  def resizeView(self):
    """ Resize columns so that everything fits into the view
    """
    self.header().resizeSections(QtGui.QHeaderView.ResizeToContents)

  def customAdded(self, newIndex):
    """ Save file and update view after new User Defined projection has been
        added at newIndex

      :param newIndex: QModelIndex
      :type newIndex: QModelIndex
      :raises:
    """
    # Update user file
    self.model().saveCustom()
    # Set current index
    self.setCurrentIndex(newIndex)
    # Clear previous selection
    self.clearSelection()
    # Select current index
    self.selectionModel().select(newIndex,
      QtGui.QItemSelectionModel.Select|QtGui.QItemSelectionModel.Rows)
    # Expand branch
    self.expand(newIndex.parent())
    self.resizeView()

  def contextMenuRequested(self, pos):
    """ Pop up a UserDefinedDialog on user request
    """
    index = self.currentIndex()
    if (index.parent() == self.rootIndex() and index.row() == 0 or
      index.parent().internalPointer() == self.model().root.children()[0]):
      # New User Defined Datum / Projection Dialog
      dlg = UserDefinedDialog(self.customProjStr)
    elif index.parent() != self.rootIndex():
      # Standard EPSG or Coordinate reference System
      node = index.internalPointer()
      dlg = SaveEPSGDialog(node.projStr(), node.epsg())
    else:
      # Coordinate reference System
      dlg = None

    if dlg:
      if dlg.exec_():
        self.customProjStr, epsgString = dlg.text()
        newIndex = self.model().addCustom(self.customProjStr, epsgString)
        self.customAdded(newIndex)

  def keyPressEvent(self, event):
    """ Custom reimplementation of the `keyPressEvent` method to delete
        records using  `Qt.Key_Delete`
    """
    if event.key() == Qt.Key_Delete:
      # Delete key was pressed, remove current item
      result = self.model().removeRecord(self.currentIndex())
      if result:
        # Update view selecting current index
        self.selectionModel().select(self.currentIndex(),
          QtGui.QItemSelectionModel.Select|QtGui.QItemSelectionModel.Rows)

        # Update user file
        self.model().saveCustom()
        return
    else:
      super(ProjView, self).keyPressEvent(event)

#===============================================================================
class ProjValidator(QtGui.QValidator):
  """ Validate string against PROJ4 syntax.

   .. note:: This validator stores its status for later usage
  """
  def __init__(self, parent):
    """ Create a new ProjValidator instance.

      :param parent: parent object
      :type parent: QObject
      :raises:
    """
    super(ProjValidator, self).__init__(parent)
    self.extraKeys = EXTRA_KEYWORDS
    self._status = QtGui.QValidator.Acceptable

  def validate(self, *args):
    """ Validate proj data string:

     .. warning:: *** THIS DOES NOT WORK AS EXPECTED ***
        Some strings seem valid but yeld unexpected results.
        This depends on the `Proj` class implementation.
    """
    status = QtGui.QValidator.Intermediate
    palette = self.parent().palette()
    buttonBox = self.parent().parent().buttonBox

    # Search for extra keyword(s)
    text = str(args[0])

    for extraKey in self.extraKeys:
      b, key, e = text.partition(extraKey)
      if key == extraKey:
        # extra keyword found, parse its value
        pn = e.split()[0]
        # Remove extra from the string
        text = text.replace(extraKey + pn, '')

    try:
      p = Proj(text)
    except (RuntimeError, UnicodeEncodeError), e:
      pass
    else:
      status = QtGui.QValidator.Acceptable

    if status == QtGui.QValidator.Acceptable:
      color = QtGui.QColor('white')
      buttonBox.button(QtGui.QDialogButtonBox.Ok).setEnabled(True)
    else:
      color = QtGui.QColor('SALMON')
      buttonBox.button(QtGui.QDialogButtonBox.Ok).setEnabled(False)

    palette.setColor(QtGui.QPalette.Base, color)
    self.parent().setPalette(palette)
    self._status = status
    return status, args[0]

  def status(self):
    """ Return True **ONLY IF** the status is `QtGui.QValidator.Acceptable`

      :returns: True if the value is `QtGui.QValidator.Acceptable`
      :rtype: bool
      :raises:
    """
    if self._status == QtGui.QValidator.Acceptable:
      return True
    else:
      return False

#===============================================================================
class UserDefinedDialog(QtGui.QDialog):
  """ A simple class to handle a User Defined Datum/Projection Proj.4 String
  """
  def __init__(self, text='', parent=None, title=CUSTOMPROJECTION):
    """ Create a new UserDefinedDialog instance.

      :param text: text of the dialog
      :type text: string
      :param parent: parent object
      :type parent: QObject
      :param title: dialog title
      :type title: string
      :raises:
    """
    super(UserDefinedDialog, self).__init__(parent)
    self.setWindowTitle(title)

    self.customEditor = QtGui.QLineEdit(text)
    self.customEditor.setValidator(ProjValidator(self.customEditor))
    self.customEditor.setToolTip(QCA.translate("UserDefinedDialog",
      "Enter here a valid PROJ.4 string\n"
      "The string is validated while you type.\n"
      "If the string is NOT valid you cannot save it.\n"
      "WARNING... a valid PROJ.4 string\n"
      "does NOT mean that it is correct!"))
    layout = QtGui.QVBoxLayout()
    label = QtGui.QLabel(QCA.translate("UserDefinedDialog",
      "Enter a valid PROJ.4 string below:"))
    label.setToolTip(QCA.translate("UserDefinedDialog",
      "Below you can define your own Datum/Projection:\n"
      "Use the standard PROJ.4 syntax plus the keywords\n"
      "+datumShift= and +geoidgrids= to define custom\n"
      "Datum Shift and Geoid (.gtx) files.\n"))
    layout.addWidget(label)
    layout.addWidget(self.customEditor)

    self.buttonBox = QtGui.QDialogButtonBox(Qt.Horizontal)
    self.buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Cancel |
      QtGui.QDialogButtonBox.Ok)
    self.buttonBox.rejected.connect(self.reject)
    self.buttonBox.accepted.connect(self.accept)

    l = QtGui.QVBoxLayout(self)
    l.setSizeConstraint(QtGui.QLayout.SetFixedSize)
    l.addLayout(layout)
    l.addWidget(self.buttonBox)
    # Validate current text string
    self.customEditor.validator().validate(text, len(text))

  def text(self):
    """ Return QLineEdit content as string

      :returns: QLineEdit content as string
      :rtype: string
      :raises:
    """
    return str(self.customEditor.text()), ''

  def closeEvent(self, event):
    """ Close dialog **only if** the validator agrees on content
    """
    if self.customEditor.validator().status():
      super(UserDefinedDialog, self).close()
    else:
      event.ignore()

#===============================================================================
class SaveEPSGDialog(QtGui.QDialog):
  """ A simple class to handle a User Defined Datum/Projection Proj.4 String
  """
  def __init__(self, text='', epsg='', parent=None, title=CUSTOMPROJECTION):
    """ Create a new SaveEPSGDialog instance.

      :param text: text
      :type text: string
      :param epsg: optional epsg string
      :type epsg: string
      :param parent: parent object
      :type parent: QObject
      :param title: dialog title
      :type title: string
      :raises:
    """
    super(SaveEPSGDialog, self).__init__(parent)
    self.setWindowTitle(title)

    self._text = text
    self._epsg = epsg

    layout = QtGui.QVBoxLayout()
    layout.addWidget(QtGui.QLabel(
      QCA.translate("SaveEPSGDialog",
       "Save current EPSG projection among user's defined?")))

    self.buttonBox = QtGui.QDialogButtonBox(Qt.Horizontal)
    self.buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Cancel |
      QtGui.QDialogButtonBox.Ok)
    self.buttonBox.rejected.connect(self.reject)
    self.buttonBox.accepted.connect(self.accept)

    l = QtGui.QVBoxLayout(self)
    l.setSizeConstraint(QtGui.QLayout.SetFixedSize)
    l.addLayout(layout)
    l.addWidget(self.buttonBox)

  def text(self):
    """ Return QLineEdit content and epsg as string

      :returns: QLineEdit content and epsg as string
      :rtype: tuple
      :raises:
    """
    if self._epsg:
      epsg = str(self._epsg)
    else:
      # Guess it from proj4 string
      s = str(self._text).upper()
      splitted = s.split('EPSG:')
      if len(splitted) > 1:
        rest = splitted[1]
        epsg = rest.split()[0]
      else:
        epsg = ''

    return str(self._text), epsg
